# --*-- coding: utf-8 --*--

import openpyxl
from openpyxl import load_workbook
from openpyxl.worksheet.worksheet import Worksheet
import csv
import pandas
import chardet
import random
from datetime import timedelta, date
import shutil
from collections import OrderedDict
import inspect
import time
from concurrent.futures import ThreadPoolExecutor
from xml.etree import ElementTree as ET
from typing import Union, NoReturn
import xlrd
import pandas as pd
from faker import Faker


class Public(object):
	"""
	说明：
		提供一些常用功能
		1. read_excel():
			读取Excel单元格数据
		2. write_excel
			写入Excel单元格数据
		3. read_csv():
			读取Csv单元格数据
		4. write_csv()
			写入Csv单元格数据
		5. get_file_coding()
			获取文件的编码格式
	"""

	@staticmethod
	def get_data_from_excel(file_path, sheet_name: Union[str, int] = 0, idx=None):
		"""
		读取excel数据
		:param file_path: excel路径  type:str
		:param sheet_name: sheet页名称  type:int or str
		:param idx: 索引  type: int
		:return: [dict1, dict2]
		"""
		try:
			df = pd.read_excel(file_path, sheet_name=sheet_name, engine="openpyxl")
		except FileNotFoundError as file_not_found_error:
			raise FileNotFoundError(f"{file_path}，该文件不存在，{file_not_found_error}")
		except ValueError as value_error:
			raise FileNotFoundError(f"{file_path}，该文件不存在 {sheet_name} sheet页，{value_error}")

		if not idx:
			# for i in df.index:
			#     a = df.columns[0:3:1])].fillna("").to_dict()
			return tuple(df.loc[i, list(df.columns[0::1])].fillna("").to_dict() for i in df.index[::])
		elif idx > 1:
			return dict(df.loc[idx - 2].fillna(""))
		else:
			raise KeyError("idx最小值为2，请确认是否输入错误")

	@staticmethod
	def _return_cloumn(ws: Worksheet, p_column: Union[str, int]) -> str:
		"""
		:param ws: sheet页 type=openpyxl.worksheet.worksheet.Worksheet
		:param p_column: 列名或列数 type=int or str
		:return: 列数转换为字母形式
		"""
		if isinstance(p_column, int):
			return openpyxl.utils.get_column_letter(p_column)  # 数字转换为字母
		elif isinstance(p_column, str):
			row_list = list(map(lambda x: x.value, list(ws[1])))  # 解析第一行
			if row_list.count(p_column) == 1:  # 相同字段名存在的个数
				for index, cell in enumerate(row_list):
					if cell == p_column:
						return openpyxl.utils.get_column_letter(index + 1)
			elif row_list.count(p_column) == 0:
				raise ValueError(f"当前sheet页“{ws.title}”不存在字段名为“{p_column}”")
			else:
				raise ValueError(f"当前sheet页“{ws.title}”存在多个字段名为“{p_column}”")
		else:
			raise TypeError(f"column参数类型应为int或str类型而不是“{type(p_column)}”，int表示第几列，str表示第一行列字段名")

	@staticmethod
	def getRowsClosNum(file, sheet):
		"""获取当前excl行数
			file为需要返回的文件名
			sheet为sheet数
		"""
		file = file
		wb = load_workbook(file)
		sheets = wb.get_sheet_names()
		sheet = sheets[sheet - 1]
		ws = wb[sheet]
		rows = ws.max_row
		columns = ws.max_column
		return rows, columns

	@staticmethod
	def read_excel(path: str, column: int = 1, row: int = 1, sheet: int = 1) -> str:
		"""
		读取Excel某一单元格数据
		:param path: Excel路径 type=str
		:param sheet: 第几个sheet页或sheet页名称 type=int or str
		:param row: 行数 type=int
		:param column: 列数或列字段名 type=int or str
		:return: 单元格的值
		"""
		# TODO: 有将第一列作为字段名的需求
		if not isinstance(row, int):
			raise TypeError(f"row参数类型应为int类型而不是“{type(row)}”")
		wb = openpyxl.load_workbook(path)
		if isinstance(sheet, str):
			ws = wb[sheet]
			column = __class__._return_cloumn(ws=ws, p_column=column)
			return ws[column + str(row)].value
		elif isinstance(sheet, int):
			ws = wb[wb.sheetnames[sheet - 1]]
			column = __class__._return_cloumn(ws=ws, p_column=column)
			return ws[column + str(row)].value
		else:
			raise TypeError(f"sheet参数类型应为int或str类型而不是“{type(sheet)}”，数字表示为第几个sheet页，字符表示sheet页名称")

	@staticmethod
	def read_excel_row(path: str, name: str, apiname: str, sheet: str) -> Union[str, int]:
		"""
		读取Excel某一单元格数据
		:param path: Excel路径 type=str
		:param sheet: 第几个sheet页或sheet页名称 type=int or str
		:param name: 字段名，标题字段名
		:param apiname: 行中值 需要获取的值对应的字段名
		:return: 单元格的值
		"""
		# TODO: 有将第一列作为字段名的需求
		if not isinstance(name, str):
			raise TypeError(f"name参数类型应为str类型而不是“{type(name)}”")
		data = pandas.read_excel(path, None)
		key = data.keys()
		if sheet in key:
			sh_data = pandas.DataFrame(pandas.read_excel(path, sheet))  # 获得每一个sheet中的内容
			if apiname in str(sh_data[name]):
				return str(sh_data.loc[sh_data[name] == apiname].iloc[0, 1])
			else:
				print(f"{name}值不在sheet页面中")
				return -1
		else:
			raise TypeError(f"sheet值不在sheet页面中，是“{type(name)}”")

	@staticmethod
	def urlread(bookname, sheetname, apiname):

		"""
		后续接口中使用url，采用此方法直接获取
		:param bookname: 接口文件所在路径
		:param sheetname: sheet页面名称
		:param apiname: 接口名称
		:return: 返回当前接口的url值
		"""
		# 打开Excel文件，读取excl文件，存储为字典
		wb = xlrd.open_workbook(bookname)
		sheet = wb.sheet_by_name(sheetname)
		dic = {}
		for i in range(sheet.nrows - 1):
			lis = []
			for j in range(sheet.ncols):
				lis.append(sheet.cell(i + 1, j).value)
			dic[sheet.cell(i + 1, 0).value] = lis
		readvalue = dic.get(apiname)
		readlist = readvalue[1]
		from src import Config
		base_url = Config.base_url
		readlist = base_url + readlist.strip()
		return readlist

	@staticmethod
	def write_excel(path: str, value: str = "", column: int = 1, row: int = 1, sheet: int = 1) -> NoReturn:
		"""
		写入Excel指定单元格数据
		:param path: Excel路径 type=str
		:param sheet: 第几个sheet页或sheet页名称 type=int or str
		:param row: 行数 type=int
		:param column: 列数或列字段名 type=int or str
		:param value: 需要写入的数据 type=str
		"""
		if not isinstance(row, int):
			raise TypeError(f"row参数类型应为int类型而不是“{type(row)}”")
		if not isinstance(value, str):
			raise TypeError(f"value参数类型应为str类型而不是“{type(value)}”")
		wb = openpyxl.load_workbook(path)
		if isinstance(sheet, str):
			ws = wb[sheet]
			column = __class__._return_cloumn(ws=ws, p_column=column)
			ws[column + str(row)].value = value
			wb.save(path)
		elif isinstance(sheet, int):
			ws = wb[wb.sheetnames[sheet - 1]]
			column = __class__._return_cloumn(ws=ws, p_column=column)
			ws[column + str(row)].value = value
			wb.save(path)
		else:
			raise TypeError(f"sheet参数类型应为int或str类型而不是“{type(sheet)}”，数字表示为第几个sheet页，字符表示sheet页名称")

	@staticmethod
	def write_excel_row(path: str, sheet: int = 1) -> NoReturn:
		"""
		写入Excel指定单元格数据
		:param path: Excel路径 type=str
		:param value: 需要写入的数据 type=str
		"""
		wb = openpyxl.load_workbook(path)

		sheet1 = wb[sheet]

		row = sheet1.max_row
		column = sheet1.max_column
		return row, column

	@staticmethod
	def read_csv(path: str, column: int = 1, row: int = 1, fields: bool = True) -> str:
		"""
		说明：读取csv某一单元格数据
		注意：文件编码建议使用utf-8
		:param path: csv文件路径 type=str
		:param row: 行数 type=int
		:param column: 字段名或列数 type=str or int
		:param fields: csv文件第一行是否是字段
		:return: 单元格值
		"""
		# TODO: 有将第一列作为字段名的需求
		if not isinstance(row, int):
			raise TypeError(f"row参数类型应为int类型而不是“{type(row)}”")
		with open(path, "r", encoding="utf-8") as csv_file:  # 转为list
			csv_reader = csv.reader(csv_file)
			csv_list = list(csv_reader)
		with open(path, "r", encoding="utf-8") as csv_file:  # 转为dict
			csv_dict = csv.DictReader(csv_file)
			csv_dict_list = list(csv_dict)
		if fields:
			if isinstance(column, str):
				return csv_dict_list[row - 1][column]
			elif isinstance(column, int):
				return csv_list[row][column - 1]
			else:
				raise TypeError(f"csv中有字段时，可以使用字段名表示列名类型为str，当然也可以使用列数表示类型为int，但当前类型为“{column}”")
		else:
			if not isinstance(column, int):
				raise TypeError(f"csv中没有字段，请使用int类型表示列数，当前类型为“{type(column)}”")
			else:
				return csv_list[row - 1][column - 1]

	@staticmethod
	def write_csv(path: str, value: str = "", column: int = 1, row: int = 1, fields: bool = True) -> NoReturn:
		"""
		说明：写入csv指定单元格数据
		注意：文件编码建议使用utf-8
		:param path: csv文件路径 type=str
		:param value: 需要写入的数据 type=str
		:param row: 行数 type=int
		:param column: 字段名或列数 type=str or int
		:param fields: csv文件第一行是否是字段
		:return: None
		"""
		if not isinstance(row, int):
			raise TypeError(f"row参数类型应为int类型而不是“{type(row)}”")
		if row == 1:
			raise ValueError("第一行为字段行，不允许修改...")
		df = pandas.read_csv(path)
		if fields:
			if isinstance(column, str):
				df.at[row - 2, column] = value  # [行数，列名]
			elif isinstance(column, int):
				df.iat[row - 2, column - 1] = value  # [行数，列数]
			else:
				raise TypeError(f"csv中有字段时，可以使用字段名表示列名类型为str，当然也可以使用列数表示类型为int，但当前类型为“{type(column)}”")
		else:
			if not isinstance(column, int):
				raise TypeError(f"csv中没有字段，请使用int类型表示列数，当前类型为“{type(column)}”")
			else:
				df.iat[row - 2, column - 1] = value
		df.to_csv(path_or_buf=path, index=False)

	@staticmethod
	def get_file_coding(filepath: str) -> dict:
		"""
		说明：
			获取文件的编码格式
		:param filepath: 文件路径 type=str
		:return: {'confidence': 1.0, 'encoding': 'UTF-8'} type=dict
		"""
		with open(file=filepath, mode="rb") as f:
			s = f.read()
		return chardet.detect(s)

	@staticmethod
	def random_phone(locale: Union[str, list] = "zh_CN") -> str:
		"""
		说明:
			生成中国大陆手机号
		:return: str
		"""
		return Faker(locale=locale).phone_number()

	@staticmethod
	def random_id_card(locale: Union[str, list] = "zh_CN") -> str:
		"""
		说明:
			生成中国大陆身份证号
		:return: str
		"""
		return Faker(locale=locale).ssn()

	@staticmethod
	def random_email(locale: Union[str, list] = "zh_CN") -> str:
		"""
		说明:
			生成邮箱
		:return: str
		"""
		return Faker(locale=locale).email()

	@staticmethod
	def random_ip_address(locale: Union[str, list] = "zh_CN", ip_type: str = "ipv4") -> str:
		"""
		说明:
			生成IP地址
		:return: str
		"""
		if ip_type == "ipv4":
			return Faker(locale=locale).ipv4()
		elif ip_type == "ipv6":
			return Faker(locale=locale).ipv6()
		else:
			raise ValueError(f"不支持 {ip_type} ip类型")

	@staticmethod
	def random_name(locale: Union[str, list] = "zh_CN"):
		"""
		生成随机姓名
		:param locale:
		:return:
		"""
		return Faker(locale=locale).name()

	@staticmethod
	def make_archive(base_name: str, for_mat: str = "zip", root_dir=None) -> str:
		"""
		说明：
			压缩指定文件夹/目录
		:param base_name: 压缩后文件名/路径
		:param for_mat: 压缩格式 "zip", "tar", "bztar", "gztar"
		:param root_dir: 被压缩目录
		:return: 压缩后文件路径
		"""
		return shutil.make_archive(
			base_name=base_name,
			format=for_mat,
			root_dir=root_dir
		)

	@staticmethod
	def unpack_archive(filename: str, extract_dir=None, for_mat=None) -> NoReturn:
		"""
		说明：
			解压指定文件
		:param for_mat: 解压格式，如果未指定则取压缩文件的后缀名格式
		:param filename: 待解压文件名/路径
		:param extract_dir: 解压到指定目录
		"""
		shutil.unpack_archive(
			filename=filename,
			extract_dir=extract_dir,
			format=for_mat
		)


def monitor_run_timeout(fn, timeout: int = 10, timeout_message: str = ""):
	"""
		说明：对于假死的线程，无法直接使用多线程/多进程的超时时间(timeout)控制
			至少目前发现ThreadPoolExecutor Executor.map() 传入timeout无法控制，依然会再子线程/子进程中超时

			目前使用的方式是，判断子线程/进程的执行时间（通过future的running状态时间），如果超过时间，则在主线程抛出异常

	:param fn: 需要监控的方法
	:param timeout: 超时时间
	:param timeout_message: 超时报错信息
	:return: 如果没有超时 返回fn执行结果
	"""
	executor = ThreadPoolExecutor(1)
	start_time = time.time()
	future = executor.submit(fn)
	# future.add_done_callback(fn_done) future执行完毕后的回调，fn_done函数参数为future(必传)

	while future.running():  # 如果future线程/进程还在执行
		run_time = time.time() - start_time
		if run_time > timeout:
			if not str.strip(timeout_message):
				raise TimeoutError("运行超时...")
			else:
				raise TimeoutError(timeout_message)
	return future.result()


class ParseXml(object):
	"""
		解析xml
		注意：保存时无法保存注释
	"""

	def __init__(self, filepath: str):
		self.filepath = filepath
		self._tree = ET.parse(self.filepath)

	def get_value_from_xpath(self, xpath: str, attribute: str) -> Union[str, None]:
		"""
		说明：
			通过xpath定位到节点，返回指定属性的值
		:param xpath: xpath定位语句
		:param attribute: 属性名
		:return: 指定节点的属性值
		:type: str or None
		"""
		self._refresh()
		ele = self.root.find(xpath)
		return ele.attrib.get(attribute, None)

	def set_value_from_xpath(self, xpath: str, attribute: str, value: str) -> None:
		"""
		说明:
			通过xpath定位到节点，并设置指定节点的属性值
		:param xpath: xpath定位语句
		:param attribute: 属性名
		:param value: 指定节点的属性值
		:return: None
		"""
		self._refresh()
		ele = self.root.find(xpath)
		ele.set(attribute, value)
		self._tree.write(self.filepath, encoding='UTF-8', xml_declaration=True, method='xml')

	def _refresh(self):
		"""
		说明：
			重新在内存加载最新的文件
		:return:
		"""
		self._tree = ET.parse(self.filepath)

	def _get_root(self):
		"""
		说明：
			返回xml文件的根节点
		:return: 根节点对象
		:rtype: <class 'Element'>
		"""
		self._refresh()
		return self._tree.getroot()

	root = property(fget=_get_root, doc="获取root节点")


if __name__ == '__main__':
	# print(Public.get_data_from_excel(r'D:\lc\pythonProject\autoTest\Data\LoginData.xlsx', "LoginData", idx=2))
	print(Public.random_name("zh_CN"))
